package edu.siu.math.egut.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;


import org.apache.commons.collections15.BidiMap;
import org.apache.commons.collections15.bidimap.DualHashBidiMap;





/**
 * The Class Unipotent models certain unipotent elements of split
 * connected reductive algebraic groups. Let G be such a group and assume that G
 * is equipped with a choice of maximal torus T, used to define roots. Let U be
 * a maximal unipotent subgroup of G which is compatible with T. Any element of
 * U can be written as a product of elements of the form x_a(r) where, for a
 * root a, x_a is an isomorphism between the one-dimensional unipotent subgroup
 * associated to a and the additive group-scheme. Such elements are called
 * "simple unipotents," or sunipotents for short. A unipotent is represented
 * simply as a product of sunipotents. Because not every word in the sunipotents
 * corresponds to a unipotent element of G, it is possible to define objects of
 * class Unipotent which are not, actually, unipotent. Some checking is done to
 * guard against this, but a certain amount of care and/or exception-handling on
 * the users part will be required.
 * 
 * 
 * @author Joe Pleso
 */
public class ExceptionalGroupElement implements Cloneable{

    public Object clone() throws CloneNotSupportedException{
	ExceptionalGroupElement clone = (ExceptionalGroupElement)super.clone();
	clone.product = new ArrayList<SimpleUnipotentElement>(clone.product);
	clone.toral = new ArrayList<LaurentPolynomial>(clone.toral);
	return clone;
    }

    /**
     * Returns the toral part of the element.
     * 
     * @return the toral part of the element
     */
    public List<LaurentPolynomial> popToralPart(){
	List<LaurentPolynomial> result =
	    new ArrayList<LaurentPolynomial> (toral);
	
	toral.clear();
	for (int i = 0; i < this.getGroup().getRank(); i++){
	    toral.add(LaurentPolynomial.create(1));
	}
	
	return result;
    }
    
    
    
    private static ExceptionalGroupElement insertToralElement (
	    ExceptionalGroupElement x,
	    int index,
	    List<LaurentPolynomial> l){
	try {
	    ExceptionalGroupElement result = (ExceptionalGroupElement) 
	    x.clone();
	    for (int i =0; i < index;i++){
		SimpleUnipotentElement old = result.product.get(i);
		result.product.set(i, 
			SimpleUnipotentElement.create(old.getRoot(),
				old.getPolynomial().times(
					result.group.rootActingOnTorus(
						old.getRoot(),l).inverse())));
	    }
	    for (int i =0; i < result.group.getRank(); i++){
		LaurentPolynomial oldf = result.toral.get(i);
		result.toral.set(i, oldf.times(l.get(i)));
	    }
	    return result;
	    
	} catch (CloneNotSupportedException e) {
	    throw new RuntimeException(e);
	    
	}
	
    }
    
    
    /**
     * Multiply two Unipotents together. The Unipotents must be from the same
     * root system.
     * 
     * @param a                the first Unipotent to be multiplied
     * @param b             the second Unipotent to be multiplied
     * @return the product ab of Unipotents a and b, with all associations 
     * of a preserved, and associations of b preserved iff they are 
     * consistent with those of a.
     * 
     */

    
    public static ExceptionalGroupElement times(
	    ExceptionalGroupElement a, ExceptionalGroupElement b) {
	if (a.getGroup() != b.getGroup()) {
	    throw new IllegalArgumentException();
	}
	a = insertToralElement(a, a.getNumberOfTerms(), b.toral);

	
	List<SimpleUnipotentElement> list = 
	    new ArrayList<SimpleUnipotentElement>();
	list.addAll(a.product);
	list.addAll(b.product);
	
	
	
	ExceptionalGroupElement c =  new ExceptionalGroupElement(
		a.getGroup(), list,a.toral);
	
	
	return c;

    }
    /**
     * Multiply a Unipotent by a Sunipotent (on the right). The Unipotents
     *  must be from the same
     * root system.
     * 
     * @param u  Unipotent 
     * @param s  Sunipotent 
     * @return the product u.s of u and s, as a Unipotent object with all
     *  associations 
     * of u preserved, no association assigned to s.
     * 
     */

    
     static ExceptionalGroupElement times(ExceptionalGroupElement u, 
	     SimpleUnipotentElement s) {
	if (u.getGroup().getRank() != s.getRoot().length) {
	    throw new IllegalArgumentException();
	}

	List<SimpleUnipotentElement> list =
	    new ArrayList<SimpleUnipotentElement>();
	list.addAll(u.product);
	list.add(s);
	
	ExceptionalGroupElement c =  
	    new ExceptionalGroupElement(u.getGroup(), list,u.toral);
	
	
	return c;

    }
   
    /**
     * Modify the current Unipotent by appending the given Sunipotent to it.
     * @param s a Sunipotent
     */
    public void rightMultiply(SimpleUnipotentElement s){
	if(s.getRoot().length != this.group.getRank()){
	    throw new IllegalArgumentException();
	}
	
	product.add(s);
	
	
    }
    private List<SimpleUnipotentElement> product;
    private final ExceptionalGroup group;
 

    
    
    
    
    
    
    

    /**
     * Create a Unipotent that has the form x_{alpha_1}(s1) x_{alpha_2}(s2)...
     * 
     * @param g               the root system in which the unipotent lives
     * @param roots                a list of roots
     * @param s              a string prefix that will have numbers
     *  appended to it
     */
     ExceptionalGroupElement(ExceptionalGroup g, int[][] roots, String s) {
	List<SimpleUnipotentElement> list = 
	    new ArrayList<SimpleUnipotentElement>();
	BidiMap<SimpleUnipotentElement, String> map = 
	    new DualHashBidiMap<SimpleUnipotentElement, String>();
	for (int i = 0; i < roots.length; i++)
	{
	    String si = s
	    + Integer.toString(i + 1);
	    SimpleUnipotentElement x;
		x = SimpleUnipotentElement.create(roots[i], 
			LaurentPolynomial.create((si)));
	    list.add(x);
	    map.put(x, si);
	    
	}
	product = list;
	group = g;    
	
	toral = new ArrayList<LaurentPolynomial>(g.getRank());
	for (int i = 0; i < g.getRank(); i++){
	    toral.add(LaurentPolynomial.create(1));
	}
    }

    /**
     * Creates a new Unipotent with the specified parameters.  
     * 
     * @param g                the group in which this Unipotent lives
     * @param l               the list of Sunipotent factors
     */
     ExceptionalGroupElement(ExceptionalGroup g, List<SimpleUnipotentElement> l, 
	     List<LaurentPolynomial> f ) {
	if (g == null)
	    throw new NullPointerException();

	group = g;
	product = new ArrayList<SimpleUnipotentElement>(l); 
	toral = new ArrayList<LaurentPolynomial>(f);

    }

     /**
     * Multiplies an exceptional group element by a Weyl group element.
     * 
     * @param w the Weyl group element
     * @param g the exceptional group element
     * @return the product
     * @throws NotARootException thrown if an index in the Weyl group element
     * is invalid
     */
    public static ExceptionalGroupElement times (
	     WeylElement w, ExceptionalGroupElement g) throws NotARootException{
	 List<Integer> word = w.getWord(); 
	 for (int i = w.getWord().size()-1; i >= 0; i--){
	     g = times(word.get(i),g);
	 }
	 return g;
     }
     
   private static ExceptionalGroupElement times (int simpleIndex,
	   ExceptionalGroupElement g) throws NotARootException{
       List<LaurentPolynomial> newToral = 
	   g.group.getSimpleReflectionToral(simpleIndex, g.toral);
       List<SimpleUnipotentElement> newProduct =
	   new ArrayList<SimpleUnipotentElement>();
       
       for (SimpleUnipotentElement x : g.product){
	   newProduct.add(SimpleUnipotentElement.create(
		   g.group.getSimpleReflectionRoot(simpleIndex, x.getRoot()),
		   x.getPolynomial().times(g.group.getSimpleReflectionSign(
			   simpleIndex, x.getRoot()))));
       }
       return new ExceptionalGroupElement(g.group,newProduct,newToral);
   }

     private List<LaurentPolynomial> toral;
  
   
   
    /**
     * Gets terms in the product.
     * 
     * @return the terms
     */
     List<SimpleUnipotentElement> getTerms(){
	return product;
    }
    
    /**
     * Inserts the new Sunipotent at the given location.
     * 
     * @param location index in which to insert the new Sunipotent
     * @param newSuni  the Sunipotent to be inserted
     */
     void add(int location, SimpleUnipotentElement newSuni) {
	product.add(location, newSuni);
    }

    

   

    /**
     * Returns the Sunipotent at the specified position in the product.
     * 
     * @param index              the index of the term to be retrieved
     * @return the term at the position specified in the list of Sunipotents
     */
     SimpleUnipotentElement get(int index) {
	return product.get(index);
    }
 
    /**
     * Returns the last element in the product.
     * 
     * @return the last element in the product
     */
    SimpleUnipotentElement getLast() {
	return product.get(product.size()-1);
    }
    
    /*private void setPoly (int i, LaurentPolynomial v)
    {
	int[] vroot = product.get(i).getRoot();
	product.set(i, SimpleUnipotentElement.create(vroot,
		v));
    }*/
    
    /*
     * Test method.
     * 
     * @param args ignored.
     */
    /*public static void main(String[] args) {
	

	ExceptionalGroup g = ExceptionalGroup.getGroup(2);
	
	String s;
	
	try {
	    s = Algorithms.fileToString("SomeG2Roots");
	} catch (FileNotFoundException e) {
	    System.out.print("File not found.");
	    System.exit(1);
	    s="[[1]]";  // to ensure that s is always initialized before its
	    //use later... though this 
	    // line would never be read...
	}
	int[][] m=new int[1][1];
	try {
	    m = Algorithms.readLiEMatrix(s);
	} catch (Exception e) {
	    e.printStackTrace();
	}
	ExceptionalGroupElement u = new ExceptionalGroupElement(g, m, "u" );
	System.out.println(u);
	System.out.println("Now let us set the second argument equal to v.");
	u.setPoly(1,LaurentPolynomial.create("v"));
	
	
	
	System.out.println(u);
	System.out.println("Now let us set it equal to the first.");
	u.setPoly(1, u.product.get(0).getPolynomial());
	System.out.println(u);
	System.out.println("While we are here lets put the Polynomial reader" +
			" to the test:  read and substitute 3x^2+2x*y^2");

	LaurentPolynomial x = LaurentPolynomial.create("x");
	LaurentPolynomial y = LaurentPolynomial.create("y");
	LaurentPolynomial a = LaurentPolynomial.create("a");
	LaurentPolynomial b = LaurentPolynomial.create("b");
	LaurentPolynomial f1 = LaurentPolynomial.create(3).times(
		x).times(x);
	f1 = f1.plus(LaurentPolynomial.create(2).times(x).times(y).times(y));
	LaurentPolynomial f2 = a.times(a) ;
	f2 = f2.plus(LaurentPolynomial.create(2).times(a).times(b));
	f2 = f2.plus(b.times(b));
	
	 {
		u.replace("u1", f1);
	    } 
	    System.out.println(u);
	     {
		u.replace("x", f2);
	    } 
	    System.out.println(u);

    }*/
    
 
    
    /**
     * Substitutes the polynomial p for the variable r in the Polynomial
     *  arguments of 
     * all of the terms of a unipotent.
     * @param r String, interpreted as variable
     * @param p Polynomial, substituted for r throughout the unipotent
     */
    public void replace( String r, LaurentPolynomial p){
	for( int i = 0 ; i < product.size(); i++){
	    LaurentPolynomial arg = get(i).getPolynomial();
	    LaurentPolynomial newArg = 
		arg.replace(r, p);
	    product.set(i, 
		    SimpleUnipotentElement.create(get(i).getRoot(), (newArg)));
	    
	    
	}
	
    }
    
    /**
     * Returns the i-th root.
     * 
     * @param i the index
     * @return the root
     */
    public int[] getRoot(int i){
	return product.get(i).getRoot();
    }
    
    /**
     * Returns the i-th polynomial.
     * 
     * @param i the index
     * @return the polynomial
     */
    public LaurentPolynomial getPolynomial (int i){
	return product.get(i).getPolynomial();
    }
    
    /**
     * Gets the number of terms in the product of Sunipotents.
     * 
     * @return the number of Sunipotents in this Unipotent
     */
    public int getNumberOfTerms() {
	return product.size();
    }

    /**
     * Returns the ambient root system in which this Unipotent lives.
     * 
     * @return the root system for which this Unipotent is an element of
     */
    public ExceptionalGroup getGroup() {
	return this.group;
    }

    /**
     * Finds the first sunipotent in the product that has the specified root. If
     * such a sunipotent cannot be found in the product, -1 is returned.
     * 
     * @param root    the root to search for
     * @return the position of the specified root or -1 if not found
     */
    public int indexOf(int[] root) {
	for (int i = 0; i < product.size(); i++)
	    if (Arrays.equals(product.get(i).getRoot(), root))
		return i;
	return -1;
    }

    /**
     * Finds the last sunipotent in the product that has the specified root. If
     * such a sunipotent cannot be found in the product, -1 is returned.
     * 
     * @param root    the root to search for
     * @return the position of the specified root or -1 if not found
     */
    public int lastIndexOf(int[] root) {
	for (int i = product.size()-1; i>-1;  i--)
	    if (Arrays.equals(product.get(i).getRoot(), root))
		return i;
	return -1;
    }

  


    /**
     * Shifts sunipotent at the given index to the left all the way to the end
     * of the list.
     * 
     * @param index  the index of the sunipotent to be pushed
     * @return the final position of the sunipotent
     */
    public int pushFarLeft(int index) {
	while (index != 0) {
	    index = pushLeft(index);
	}
	return index;
    }

    public boolean equals( Object o){
	if (o.getClass() != this.getClass())
	    return false;
	ExceptionalGroupElement u = (ExceptionalGroupElement) o;
	
	if(u.getNumberOfTerms() != product.size())
	    return false;
	    else for(int i=0; i < product.size(); i++){
		if( !u.get(i).equals(get(i)))
		    return false;
	    }
	return true;
    }
    
    /**
     * Pushes the given sunipotent all the way to the left, removes it, then
     * calls cleanUp.
     * 
     * @param index           the sunipotent to be moved.
     */
    public void pushFarLeftAndRemove(int index) {
	remove(pushFarLeft(index));
	    }

    /**
     * Shifts sunipotent at the given index to the right all the way to the end
     * of the list.
     * 
     * @param index              the index of the sunipotent to be pushed
     * @return the final position of the sunipotent
     */
    public int pushFarRight(int index) {
	while (index != product.size() - 1) {
	    index = pushRight(index);
	}
	return index;
    }

    /**
     * Pushes the given sunipotent all the way to the right, removes it, then
     * calls cleanUp.
     * 
     * @param index            the sunipotent to be moved.
     */
    public void pushFarRightAndRemove(int index) {
	remove(pushFarRight(index));
	
    }

    /**
     * Shifts Sunipotent at given index to the left by one. If this commutation
     * requires the addition of new terms, they are properly inserted.
     * 
     * 
     * @param index the element to be pushed left
     * @return the location of the element that was pushed left, except
     * in the case of cancellation.  in case of cancellation, the 
     * value returned is index-1, while the Sunipotent pushed is gone.
     * (Note that if the last two terms cancel, this will NOT be the 
     * index of an element of the unipotent).
     */

    public int pushLeft(int index) {
	index--;
	if (index >= product.size() - 1 || index < 0)
	    throw new IndexOutOfBoundsException();
	int[] a = product.get(index).getRoot();
	int[] b = product.get(index + 1).getRoot();
	if (Arrays.equals(a, b)) {
	    LaurentPolynomial newPoly = product.get(index).getPolynomial().plus(
		    product.get(index + 1).getPolynomial());
	    if (newPoly != LaurentPolynomial.create(0)) {
		/* replace Suni at index with a new Suni.
		 * (simply modifying the polynomial of this Sunipotent will
		 *  break old code
		 *  which does something while product contains existing
		 *   one... )
		 */  
		// manage associations 
		SimpleUnipotentElement newSuni = SimpleUnipotentElement.create(
			product.get(index)
			.getRoot(), newPoly);
		

		product.set(index, newSuni);
		// suni at index absorbs the one at index+1, so remove the
		//one at index+1
		remove(index + 1);
		
	    }
	    else // terms cancel
	    {
		remove(index+1);
		remove(index);
	    }
	    return index;
	}

	List<SimpleUnipotentElement> ab = group.commutator(product.get(index),
		product
		.get(index + 1));
	product.addAll(index, ab);

	index = index + ab.size();
	Collections.swap(product, index, index + 1);
	return index;
    }

    /**
     * Shifts Sunipotent at given index to the right by one. If this commutation
     * requires the addition of new terms, they are properly inserted.
     * 
     * 
     * @param index the index of the term to be pushed to the right by one
     * @return the location of the element that was shifted right, except 
     * in the case of cancellation. In that case, it returns index.
     * (Note that if the last two terms cancel, this will NOT be the 
     * index of an element of the unipotent).
     */

    public int pushRight(int index) {
	if (index >= product.size() - 1 || index < 0)
	    throw new IndexOutOfBoundsException();
	int[] a = product.get(index).getRoot();
	int[] b = product.get(index + 1).getRoot();
	if (Arrays.equals(a, b)) {
	    LaurentPolynomial newPoly = product.get(index).getPolynomial().plus(
		    product.get(index + 1).getPolynomial());
	    if (newPoly != LaurentPolynomial.create(0)) {
		SimpleUnipotentElement newSuni = SimpleUnipotentElement.create(
			product.get(index)
			.getRoot(), newPoly);
		

		
		product.set(index, newSuni);
		remove(index + 1);
	    }
	    else
	    {
		remove( index+1);
		remove(index);
	    }
	    return index;
	}

	List<SimpleUnipotentElement> ab = group.commutator(product.get(index),
		product
		.get(index + 1));
	product.addAll(index, ab);

	index = index + ab.size();

	Collections.swap(product, index, index + 1);
	return index + 1;

    }
    
    /**
     * @param from index of sunipotent to move
     * @param to location it should be moved to.  more precisely, this should
     *  be the index of the first existing 
     * sunipotent which should be after the one we're moving after we move
     *  it.  if desired location is last, then 
     * will be product.size()+1.
     */
    public void move(int from , int to ){
	if(to > product.size()-1)
	    pushFarRight(from);
	else {
	    SimpleUnipotentElement movedOne = product.get(from);
	    SimpleUnipotentElement currentlyAtTo = product.get(to);

	    boolean done = false;
	    while (!done) {
		done = true;
		if (product.indexOf(movedOne) > product.indexOf(currentlyAtTo))
		{
		    this.pushLeft(product.indexOf(movedOne));
		    done = false;
		}
		if (product.indexOf(movedOne) < product.indexOf(currentlyAtTo)
			-1) {
		    done = false;
		    this.pushRight(product.indexOf(movedOne));
		}
	    }// end while not done
	}// end if to > product.size() else
	
    }

    /**
     * Removes the i-th term in this Unipotent.
     * 
     * @param i the index of the term to be removed.
     */
    public void remove(int i) {
	
	product.remove(i);
    }

    
    
    /**
     * Returns a string representation of this Unipotent.
     * 
     * @see java.lang.Object#toString()
     */

    public String toString() {
	StringBuffer pre = new StringBuffer ("h(");
	for (LaurentPolynomial f : toral){
	    pre.append(f);
	    pre.append(",\t");
	}
	pre.deleteCharAt(pre.length()-1);
	pre.deleteCharAt(pre.length()-1);
	pre.append(")\n");
	if (product.size() != 0) {
	    int currentLineLength = 0;
	    StringBuffer sb = new StringBuffer(pre);
	    
	    for (int i = 0 ; i < product.size(); i++) {
		if( currentLineLength + product.get(i).toString().length() 
			> 80 )//- 9)
		{
		    sb.append("\n");
		    currentLineLength = 0;
		}// end if currentlinelength + .. > .. 
		sb.append(product.get(i).toString()+"\t" );
		currentLineLength += 9 + product.get(i).toString().length();
	    }
	    return sb.toString();
	} else {
	    return pre.toString();

	}
    }
    
    /**
     * Returns a string representation of this Unipotent.
     * @return a TeX string
     * 
     * 
     */

    public String toTex() {
	StringBuffer pre = new StringBuffer();// ("h(");
	/*for (LaurentPolynomial f : toral){
	    pre.append(f);
	    pre.append(",\t");
	}*/
	//pre.deleteCharAt(pre.length()-1);
	//pre.deleteCharAt(pre.length()-1);
	//pre.append(")\n");
	if (product.size() != 0) {
	    int currentLineLength = 0;
	    StringBuffer sb = (pre);
	    
	    for (int i = 0 ; i < product.size(); i++) {
		if( currentLineLength + product.get(i).toString().length() 
			> 80 )//- 9)
		{
		    sb.append("\n");
		    currentLineLength = 0;
		}// end if currentlinelength + .. > .. 
		sb.append(product.get(i).toTex()+"\t" );
		currentLineLength += 9 + product.get(i).toString().length();
	    }
	    sb.append("\\\\");
	    return sb.toString();
	} else {
	    return pre.toString();

	}
    }
       
    /*
     * Returns a string representation of this Unipotent in a tabular 
     * format with the index of each Sunipotent indicated.f
     * 
     *  look around for something to facilitate output that looks
     *  at how long the lines are and puts breaks when it 
     * needs to.
     * @return the string
     * 
     * @see java.lang.Object#toString()
     *

    public String toStringWithIndices() {
	
	if (product.size() != 0) {
	    int currentLineLength = 0;
	    StringBuffer sb = new StringBuffer();
	    for (int i = 0 ; i < product.size(); i++) {
		if( currentLineLength + product.get(i).toString().length()
			> edu.siu.math.egut.io.Algorithms.LINELENGTH - 9)
		{
		    sb.append("\n");
		    currentLineLength = 0;
		}// end if currentlinelength + .. > .. 
		sb.append(i + " "+product.get(i).toString()+"\t" );
		currentLineLength += 9 + product.get(i).toString().length();
	    }
	    return sb.toString();
	} else {
	    return "(empty unipotent)";

	}
    }*/
    
    
   
   
    

}
